# ZonedDateTimeMapping.java

    package org.codefilarete.stalactite.mapping;

    import java.time.LocalDateTime;
    import java.time.ZoneId;
    import java.time.ZonedDateTime;
    import java.util.Collections;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Map.Entry;
    import java.util.Set;
    import javax.annotation.Nullable;

    import org.codefilarete.reflection.PropertyAccessPoint;
    import org.codefilarete.reflection.PropertyMutator;
    import org.codefilarete.reflection.ReadWritePropertyAccessPoint;
    import org.codefilarete.stalactite.sql.Dialect;
    import org.codefilarete.stalactite.sql.ddl.SqlTypeRegistry;
    import org.codefilarete.stalactite.sql.ddl.structure.Column;
    import org.codefilarete.stalactite.sql.ddl.structure.Table;
    import org.codefilarete.stalactite.sql.result.ColumnedRow;
    import org.codefilarete.stalactite.sql.statement.binder.ParameterBinderRegistry;
    import org.codefilarete.tool.Reflections;
    import org.codefilarete.tool.collection.Arrays;
    import org.codefilarete.tool.exception.NotImplementedException;
    import org.codefilarete.tool.function.Predicates;

    /**
     * A mapping strategy to persist a {@link ZonedDateTime} : requires 2 columns, one for the date-time part, another for the timezone.
     * Columns must respectively have a Java type of :
     * <ul>
     * <li>{@link LocalDateTime}</li>
     * <li>{@link ZoneId}</li>
     * </ul>
     * Thus, the {@link SqlTypeRegistry} and {@link ParameterBinderRegistry}
     * of your {@link Dialect} must have them registered (which is done by default).
     *
     * @author Guillaume Mary
     */
    public class ZonedDateTimeMapping<T extends Table<T>> implements EmbeddedBeanMapping<ZonedDateTime, T> {

    	private final Column<T, LocalDateTime> dateTimeColumn;
    	private final Column<T, ZoneId> zoneColumn;
    	private final UpwhereColumn<T> dateTimeUpdateColumn;
    	private final UpwhereColumn<T> zoneUpdateColumn;
    	private final Set<Column<T, ?>> columns;
    	private final ZonedDateTimeToBeanRowTransformer zonedDateTimeRowTransformer;

    	/**
    	 * Builds a {@link LocalDateTime} and {@link ZoneId} embedded mapping by specifying repective {@link Column}s.
    	 * {@link Column}s are expected to be from same table, no strong control is made about that except generic type, caller must be aware of it.
    	 *
    	 * @param dateTimeColumn the column containing date and time part of final {@link ZonedDateTime}
    	 * @param zoneColumn the column containing the zone part of final {@link ZonedDateTime}
    	 * @throws IllegalArgumentException if dateTimeColumn is not of type {@link LocalDateTime} or zoneColumn of type {@link ZoneId}
    	 */
    	public ZonedDateTimeMapping(Column<T, LocalDateTime> dateTimeColumn, Column<T, ZoneId> zoneColumn) {
    		if (!LocalDateTime.class.isAssignableFrom(dateTimeColumn.getJavaType())) {
    			throw new IllegalArgumentException("Only columns with type " + Reflections.toString(LocalDateTime.class) + " are supported");
    		}
    		if (!ZoneId.class.isAssignableFrom(zoneColumn.getJavaType())) {
    			throw new IllegalArgumentException("Only columns with type " + Reflections.toString(ZoneId.class) + " are supported");
    		}
    		this.dateTimeColumn = dateTimeColumn;
    		this.zoneColumn = zoneColumn;
    		this.dateTimeUpdateColumn = new UpwhereColumn<>(dateTimeColumn, true);
    		this.zoneUpdateColumn = new UpwhereColumn<>(zoneColumn, true);
    		this.columns = Collections.unmodifiableSet(Arrays.asHashSet(dateTimeColumn, zoneColumn));
    		this.zonedDateTimeRowTransformer = new ZonedDateTimeToBeanRowTransformer();
    	}

    	@Override
    	public Set<Column<T, ?>> getColumns() {
    		return columns;
    	}

    	@Override
    	public RowTransformer<ZonedDateTime> getRowTransformer() {
    		return this.zonedDateTimeRowTransformer;
    	}

    	@Override
    	public void addPropertySetByConstructor(PropertyAccessPoint<ZonedDateTime, ?> accessor) {
    		// this class doesn't support bean factory so it can't support properties set by constructor
    	}

    	@Override
    	public Map<Column<T, ?>, ?> getInsertValues(ZonedDateTime zonedDateTime) {
    		Map<Column<T, ?>, Object> result = new HashMap<>();
    		result.put(dateTimeColumn, zonedDateTime.toLocalDateTime());
    		result.put(zoneColumn, zonedDateTime.getZone());
    		return result;
    	}

    	@Override
    	public Map<UpwhereColumn<T>, ?> getUpdateValues(ZonedDateTime modified, ZonedDateTime unmodified, boolean allColumns) {
    		Map<Column<T, ?>, Object> unmodifiedColumns = new HashMap<>();
    		Map<UpwhereColumn<T>, Object> toReturn = new HashMap<>();
    		// getting differences side by side
    		if (modified != null) {
    			LocalDateTime modifiedDateTime = unmodified == null ? null : unmodified.toLocalDateTime();
    			if (!Predicates.equalOrNull(modified.toLocalDateTime(), modifiedDateTime)) {
    				toReturn.put(dateTimeUpdateColumn, modified.toLocalDateTime());
    			} else {
    				unmodifiedColumns.put(dateTimeColumn, modifiedDateTime);
    			}
    			ZoneId modifiedZone = unmodified == null ? null : unmodified.getZone();
    			if (!Predicates.equalOrNull(modified.getZone(), modifiedZone)) {
    				toReturn.put(zoneUpdateColumn, modified.getZone());
    			} else {
    				unmodifiedColumns.put(zoneColumn, modifiedZone);
    			}
    		} else {
    			toReturn.put(dateTimeUpdateColumn, null);
    			toReturn.put(zoneUpdateColumn, null);
    		}

    		// adding complementary columns if necessary
    		if (!toReturn.isEmpty() && allColumns) {
    			for (Entry<Column<T, ?>, Object> unmodifiedField : unmodifiedColumns.entrySet()) {
    				toReturn.put(new UpwhereColumn<>(unmodifiedField.getKey(), true), unmodifiedField.getValue());
    			}
    		}
    		return toReturn;
    	}

    	@Override
    	public ZonedDateTime transform(ColumnedRow row) {
    		return zonedDateTimeRowTransformer.transform(row);
    	}

    	@Override
    	public Map<ReadWritePropertyAccessPoint<ZonedDateTime, ?>, Column<T, ?>> getPropertyToColumn() {
    		throw new NotImplementedException(Reflections.toString(ZonedDateTimeMapping.class) + " can't export a mapping between some accessors and their columns"
    				+ " because properties of " + Reflections.toString(ZonedDateTime.class) + " can't be set");
    	}

    	@Override
    	public Map<PropertyMutator<ZonedDateTime, ?>, Column<T, ?>> getReadonlyPropertyToColumn() {
    		throw new NotImplementedException(Reflections.toString(ZonedDateTimeMapping.class) + " can't export a mapping between some accessors and their columns"
    				+ " because properties of " + Reflections.toString(ZonedDateTime.class) + " can't be set");
    	}

    	@Override
    	public Set<Column<T, ?>> getWritableColumns() {
    		return this.columns;
    	}

    	@Override
    	public Set<Column<T, ?>> getReadonlyColumns() {
    		return java.util.Collections.emptySet();
    	}

    	@Nullable
    	private ZonedDateTime buildZonedDateTime(ColumnedRow columnedRow) {
    		return buildZonedDateTime(columnedRow.get(dateTimeColumn), columnedRow.get(zoneColumn));
    	}

    	@Nullable
    	private ZonedDateTime buildZonedDateTime(LocalDateTime dateTimeColumnName, ZoneId zoneColumnName) {
    		if (dateTimeColumnName == null || zoneColumnName == null) {
    			return null;
    		} else {
    			return ZonedDateTime.of(dateTimeColumnName, zoneColumnName);
    		}
    	}

    	class ZonedDateTimeToBeanRowTransformer extends ToBeanRowTransformer<ZonedDateTime> {

    		public ZonedDateTimeToBeanRowTransformer() {
    			super(ZonedDateTime.class, Collections.emptyMap());
    		}

    		@Nullable
    		@Override
    		public ZonedDateTime newBeanInstance(ColumnedRow row) {
    			return buildZonedDateTime(row);
    		}
    	}
    }